Skip to content

fix(email): add SMTP client timeouts to prevent indefinite hanging#51

Merged
ilramdhan merged 2 commits intomutugading:mainfrom
ilramdhan:feat/formula-master-be
Apr 14, 2026
Merged

fix(email): add SMTP client timeouts to prevent indefinite hanging#51
ilramdhan merged 2 commits intomutugading:mainfrom
ilramdhan:feat/formula-master-be

Conversation

@ilramdhan
Copy link
Copy Markdown
Member

Description

This pull request improves the reliability and robustness of the SMTP email sending service by introducing explicit connection and operation timeouts, ensuring that email sending cannot hang indefinitely and will fail fast in case of network issues. Additionally, minor code improvements were made for error handling.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that changes existing API)
  • ♻️ Refactor (code change without new feature or bug fix)
  • 📚 Documentation update
  • 🧪 Test update
  • 🔧 Chore (dependencies, config, etc.)

Service(s) Affected

  • Finance Service
  • IAM Service
  • Shared Proto (gen/)
  • Root/Common

Changes Made

Timeout and reliability improvements:

  • Introduced two new constants, smtpDialTimeout and smtpOverallTimeout, to enforce strict timeouts for SMTP connection establishment and overall operation duration, preventing hangs in the email sending path (service.go).
  • Updated the sendTLS method to use a context with a bounded timeout (smtpOverallTimeout) and set a dialer timeout (smtpDialTimeout) for connecting to the SMTP server, ensuring both connection and operation are time-limited (service.go). [1] [2]
  • After establishing the connection, set a TCP-level deadline based on the context to prevent stalled servers from hanging the process during reads or writes (service.go).

Code quality improvements:

  • Updated the header formatting loop in the send method to ignore errors from fmt.Fprintf, as failures there are non-critical (service.go).

Lint & Build

  • golangci-lint run ./... passes
  • go build ./... succeeds
  • go test -race ./... passes

Pre-merge Checklist

  • I have read and followed RULES.md
  • I have read and followed CONTRIBUTING.md
  • Clean Architecture principles followed
  • All errors are properly handled
  • Context is passed appropriately
  • Structured logging is used
  • No hardcoded secrets
  • PR description is complete and clear
  • CI checks are passing

@ilramdhan ilramdhan requested a review from Copilot April 14, 2026 03:02
@ilramdhan ilramdhan self-assigned this Apr 14, 2026
@ilramdhan ilramdhan added the bug Something isn't working label Apr 14, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves IAM’s SMTP email delivery robustness by adding explicit timeouts/deadlines to prevent the TLS email-sending path from hanging indefinitely during network stalls.

Changes:

  • Added SMTP dial and overall-operation timeout constants.
  • Updated TLS SMTP sending to use a context-bounded deadline plus TCP connection deadlines.
  • Adjusted header formatting to ignore fmt.Fprintf’s return values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +22
// SMTP client timeouts. Kept tight so failures surface fast and never block the request path.
const (
smtpDialTimeout = 10 * time.Second
smtpOverallTimeout = 30 * time.Second
)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims the SMTP timeouts "never block the request path", but the code can still block the caller for up to smtpOverallTimeout (and up to smtpDialTimeout during connect). Consider rewording to reflect that the goal is to bound/limit blocking time rather than eliminate it entirely.

Copilot uses AI. Check for mistakes.
}

client, err := smtp.NewClient(conn, s.cfg.SMTPHost)
if err != nil {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If smtp.NewClient returns an error, the underlying conn is not closed, which can leak file descriptors on handshake/banner failures. Close conn on this error path (or defer conn.Close immediately after DialContext and cancel the defer when the client takes ownership).

Suggested change
if err != nil {
if err != nil {
_ = conn.Close()

Copilot uses AI. Check for mistakes.
Comment on lines 106 to +110
func (s *Service) sendTLS(ctx context.Context, addr string, auth smtp.Auth, to, msg string) error {
// Enforce a bounded overall deadline so no SMTP step can hang indefinitely.
ctx, cancel := context.WithTimeout(ctx, smtpOverallTimeout)
defer cancel()

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeouts are only enforced in sendTLS; the non-TLS path still uses smtp.SendMail (which dials without timeouts) and can still hang indefinitely. If the intent is truly to ensure email sending never hangs, consider adding the same dial/deadline approach to the non-TLS path (custom net.Dialer + conn.SetDeadline + smtp.NewClient).

Copilot uses AI. Check for mistakes.
@ilramdhan ilramdhan merged commit 020a81a into mutugading:main Apr 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants